# # pylint: disable=too-many-lines
import pathlib
from argparse import ArgumentTypeError
from collections import defaultdict
from inspect import getsourcefile
from typing import Union, List, Dict, Callable

import pytest
from solc_select.solc_select import valid_version as solc_valid_version

from slither import Slither
from slither.core.cfg.node import Node, NodeType
from slither.core.declarations import Function, Contract
from slither.core.solidity_types import ArrayType, ElementaryType
from slither.core.variables.local_variable import LocalVariable
from slither.core.variables.state_variable import StateVariable
from slither.slithir.operations import (
    OperationWithLValue,
    Phi,
    Assignment,
    HighLevelCall,
    Return,
    Operation,
    Binary,
    BinaryType,
    InternalCall,
    Index,
    InitArray,
    NewArray,
)
from slither.slithir.utils.ssa import is_used_later
from slither.slithir.variables import (
    Constant,
    ReferenceVariable,
    LocalIRVariable,
    StateIRVariable,
    TemporaryVariableSSA,
)

# Directory of currently executing script. Will be used as basis for temporary file names.
SCRIPT_DIR = pathlib.Path(getsourcefile(lambda: 0)).parent  # type:ignore


def valid_version(ver: str) -> bool:
    """Wrapper function to check if the solc-version is valid

    The solc_select function raises and exception but for checks below,
    only a bool is needed.
    """
    try:
        solc_valid_version(ver)
        return True
    except ArgumentTypeError:
        return False


def have_ssa_if_ir(function: Function) -> None:
    """Verifies that all nodes in a function that have IR also have SSA IR"""
    for n in function.nodes:
        if n.irs:
            assert n.irs_ssa


# pylint: disable=too-many-branches, too-many-locals
def ssa_basic_properties(function: Function) -> None:
    """Verifies that basic properties of ssa holds

    1. Every name is defined only once
    2. A l-value is never index zero - there is always a zero-value available for each var
    3. Every r-value is at least defined at some point
    4. The number of ssa defs is >= the number of assignments to var
    5. Function parameters SSA are stored in function.parameters_ssa
       - if function parameter is_storage it refers to a fake variable
    6. Function returns SSA are stored in function.returns_ssa
        - if function return is_storage it refers to a fake variable
    """
    ssa_lvalues = set()
    ssa_rvalues = set()
    lvalue_assignments: Dict[str, int] = {}

    for n in function.nodes:
        for ir in n.irs:
            if isinstance(ir, OperationWithLValue) and ir.lvalue:
                name = ir.lvalue.name
                if name is None:
                    continue
                if name in lvalue_assignments:
                    lvalue_assignments[name] += 1
                else:
                    lvalue_assignments[name] = 1

        for ssa in n.irs_ssa:
            if isinstance(ssa, OperationWithLValue):
                # 1
                assert ssa.lvalue not in ssa_lvalues
                ssa_lvalues.add(ssa.lvalue)

                # 2 (if Local/State Var)
                ssa_lvalue = ssa.lvalue
                if isinstance(ssa_lvalue, (StateIRVariable, LocalIRVariable)):
                    assert ssa_lvalue.index > 0

            for rvalue in filter(
                lambda x: not isinstance(x, (StateIRVariable, Constant)), ssa.read
            ):
                ssa_rvalues.add(rvalue)

    # 3
    # Each var can have one non-defined value, the value initially held. Typically,
    # var_0, i_0, state_0 or similar.
    undef_vars = set()
    for rvalue in ssa_rvalues:
        if rvalue not in ssa_lvalues:
            assert rvalue.non_ssa_version not in undef_vars
            undef_vars.add(rvalue.non_ssa_version)

    # 4
    ssa_defs: Dict[str, int] = defaultdict(int)
    for v in ssa_lvalues:
        if v and v.name:
            ssa_defs[v.name] += 1

    for (k, count) in lvalue_assignments.items():
        assert ssa_defs[k] >= count

    # Helper 5/6
    def check_property_5_and_6(
        variables: List[LocalVariable], ssavars: List[LocalIRVariable]
    ) -> None:
        for var in filter(lambda x: x.name, variables):
            ssa_vars = [x for x in ssavars if x.non_ssa_version == var]
            assert len(ssa_vars) == 1
            ssa_var = ssa_vars[0]
            assert var.is_storage == ssa_var.is_storage
            if ssa_var.is_storage:
                assert len(ssa_var.refers_to) == 1
                assert ssa_var.refers_to[0].location == "reference_to_storage"

    # 5
    check_property_5_and_6(function.parameters, function.parameters_ssa)

    # 6
    check_property_5_and_6(function.returns, function.returns_ssa)


def ssa_phi_node_properties(f: Function) -> None:
    """Every phi-function should have as many args as predecessors

    This does not apply if the phi-node refers to state variables,
    they make use os special phi-nodes for tracking potential values
    a state variable can have
    """
    for node in f.nodes:
        for ssa in node.irs_ssa:
            if isinstance(ssa, Phi):
                n = len(ssa.read)
                if not isinstance(ssa.lvalue, StateIRVariable):
                    assert len(node.fathers) == n


# TODO (hbrodin): This should probably go into another file, not specific to SSA
def dominance_properties(f: Function) -> None:
    """Verifies properties related to dominators holds

    1. Every node have an immediate dominator except entry_node which have none
    2. From every node immediate dominator there is a path via its successors to the node
    """

    def find_path(from_node: Node, to: Node) -> bool:
        visited = set()
        worklist = list(from_node.sons)
        while worklist:
            first, *worklist = worklist
            if first == to:
                return True
            visited.add(first)
            for successor in first.sons:
                if successor not in visited:
                    worklist.append(successor)
        return False

    for node in f.nodes:
        if node is f.entry_point:
            assert node.immediate_dominator is None
        else:
            assert node.immediate_dominator is not None
            assert find_path(node.immediate_dominator, node)


def phi_values_inserted(f: Function) -> None:
    """Verifies that phi-values are inserted at the right places

    For every node that has a dominance frontier, any def (including
    phi) should be a phi function in its dominance frontier
    """

    def have_phi_for_var(
        node: Node, var: Union[StateIRVariable, LocalIRVariable, TemporaryVariableSSA]
    ) -> bool:
        """Checks if a node has a phi-instruction for var

        The ssa version would ideally be checked, but then
        more data flow analysis would be needed, for cases
        where a new def for var is introduced before reaching
        DF
        """
        non_ssa = var.non_ssa_version
        for ssa in node.irs_ssa:
            if isinstance(ssa, Phi):
                if non_ssa in map(
                    lambda ssa_var: ssa_var.non_ssa_version,
                    [
                        r
                        for r in ssa.read
                        if isinstance(r, (StateIRVariable, LocalIRVariable, TemporaryVariableSSA))
                    ],
                ):
                    return True
        return False

    for node in filter(lambda n: n.dominance_frontier, f.nodes):
        for df in node.dominance_frontier:
            for ssa in node.irs_ssa:
                if isinstance(ssa, OperationWithLValue):
                    ssa_lvalue = ssa.lvalue
                    if isinstance(
                        ssa_lvalue, (StateIRVariable, LocalIRVariable, TemporaryVariableSSA)
                    ) and is_used_later(node, ssa_lvalue):
                        assert have_phi_for_var(df, ssa_lvalue)


def verify_properties_hold(slither: Slither) -> None:
    """Ensures that basic properties of SSA hold true"""

    def verify_func(func: Function) -> None:
        have_ssa_if_ir(func)
        phi_values_inserted(func)
        ssa_basic_properties(func)
        ssa_phi_node_properties(func)
        dominance_properties(func)

    def verify(slither: Slither) -> None:
        for cu in slither.compilation_units:
            for func in cu.functions_and_modifiers:
                _dump_function(func)
                verify_func(func)
            for contract in cu.contracts:
                for f in contract.functions:
                    if f.is_constructor or f.is_constructor_variables:
                        _dump_function(f)
                        verify_func(f)

    assert isinstance(slither, Slither)
    verify(slither)


def _dump_function(f: Function) -> None:
    """Helper function to print nodes/ssa ir for a function or modifier"""
    print(f"---- {f.name} ----")
    for n in f.nodes:
        print(n)
        for ir in n.irs_ssa:
            print(f"\t{ir}")
    print("")


def _dump_functions(c: Contract) -> None:
    """Helper function to print functions and modifiers of a contract"""
    for f in c.functions_and_modifiers:
        _dump_function(f)


def get_filtered_ssa(f: Union[Function, Node], flt: Callable) -> List[Operation]:
    """Returns a list of all ssanodes filtered by filter for all nodes in function f"""
    if isinstance(f, Function):
        return [ssanode for node in f.nodes for ssanode in node.irs_ssa if flt(ssanode)]

    assert isinstance(f, Node)
    return [ssanode for ssanode in f.irs_ssa if flt(ssanode)]


def get_ssa_of_type(f: Union[Function, Node], ssatype) -> List[Operation]:
    """Returns a list of all ssanodes of a specific type for all nodes in function f"""
    return get_filtered_ssa(f, lambda ssanode: isinstance(ssanode, ssatype))


def test_multi_write(slither_from_solidity_source) -> None:
    source = """
    pragma solidity ^0.8.11;
    contract Test {
    function multi_write(uint val) external pure returns(uint) {
        val = 1;
        val = 2;
        val = 3;
    }
    }"""
    with slither_from_solidity_source(source) as slither:
        verify_properties_hold(slither)


def test_single_branch_phi(slither_from_solidity_source) -> None:
    source = """
        pragma solidity ^0.8.11;
        contract Test {
        function single_branch_phi(uint val) external pure returns(uint) {
            if (val == 3) {
                val = 9;
            }
            return val;
        }
        }
        """
    with slither_from_solidity_source(source) as slither:
        verify_properties_hold(slither)


def test_basic_phi(slither_from_solidity_source) -> None:
    source = """
    pragma solidity ^0.8.11;
    contract Test {
    function basic_phi(uint val) external pure returns(uint) {
        if (val == 3) {
            val = 9;
        } else {
            val = 1;
        }
        return val;
    }
    }
    """
    with slither_from_solidity_source(source) as slither:
        verify_properties_hold(slither)


def test_basic_loop_phi(slither_from_solidity_source) -> None:
    source = """
    pragma solidity ^0.8.11;
    contract Test {
    function basic_loop_phi(uint val) external pure returns(uint) {
        for (uint i=0;i<128;i++) {
            val = val + 1;
        }
        return val;
    }
    }
    """
    with slither_from_solidity_source(source) as slither:
        verify_properties_hold(slither)


@pytest.mark.xfail(strict=True, reason="Fails in current slither version. Fix in #1102.")
def test_phi_propagation_loop(slither_from_solidity_source):
    source = """
     pragma solidity ^0.8.11;
     contract Test {
     function looping(uint v) external pure returns(uint) {
        uint val = 0;
        for (uint i=0;i<v;i++) {
            if (val > i) {
                val = i;
            } else {
                val = 3;
            }
        }
        return val;
    }
    }
    """
    with slither_from_solidity_source(source) as slither:
        verify_properties_hold(slither)


@pytest.mark.xfail(strict=True, reason="Fails in current slither version. Fix in #1102.")
def test_free_function_properties(slither_from_solidity_source):
    source = """
        pragma solidity ^0.8.11;

        function free_looping(uint v) returns(uint) {
           uint val = 0;
           for (uint i=0;i<v;i++) {
               if (val > i) {
                   val = i;
               } else {
                   val = 3;
               }
           }
           return val;
       }

       contract Test {}
       """
    with slither_from_solidity_source(source) as slither:
        verify_properties_hold(slither)


def test_ssa_inter_transactional(slither_from_solidity_source) -> None:
    source = """
    pragma solidity ^0.8.11;
    contract A {
        uint my_var_A;
        uint my_var_B;

        function direct_set(uint i) public {
            my_var_A = i;
        }

        function direct_set_plus_one(uint i) public {
            my_var_A = i + 1;
        }

        function indirect_set() public {
            my_var_B = my_var_A;
        }
    }
    """
    with slither_from_solidity_source(source) as slither:
        c = slither.contracts[0]
        variables = c.variables_as_dict
        funcs = c.available_functions_as_dict()
        direct_set = funcs["direct_set(uint256)"]
        # Skip entry point and go straight to assignment ir
        assign1 = direct_set.nodes[1].irs_ssa[0]
        assert isinstance(assign1, Assignment)

        assign2 = direct_set.nodes[1].irs_ssa[0]
        assert isinstance(assign2, Assignment)

        indirect_set = funcs["indirect_set()"]
        phi = indirect_set.entry_point.irs_ssa[0]
        assert isinstance(phi, Phi)
        # phi rvalues come from 1, initial value of my_var_a and 2, assignment in direct_set
        assert len(phi.rvalues) == 3
        assert all(x.non_ssa_version == variables["my_var_A"] for x in phi.rvalues)
        assert assign1.lvalue in phi.rvalues
        assert assign2.lvalue in phi.rvalues


@pytest.mark.xfail(strict=True, reason="Fails in current slither version. Fix in #1102.")
def test_ssa_phi_callbacks(slither_from_solidity_source):
    source = """
    pragma solidity ^0.8.11;
    contract A {
        uint my_var_A;
        uint my_var_B;

        function direct_set(uint i) public {
            my_var_A = i;
        }

        function use_a() public {
            // Expect a phi-node here
            my_var_B = my_var_A;
            B b = new B();
            my_var_A = 3;
            b.do_stuff();
            // Expect a phi-node here
            my_var_B = my_var_A;
        }
    }

    contract B {
        function do_stuff() public returns (uint) {
            // This could be calling back into A
        }
    }
    """
    with slither_from_solidity_source(source) as slither:
        c = slither.get_contract_from_name("A")[0]
        _dump_functions(c)
        f = [x for x in c.functions if x.name == "use_a"][0]
        var_a = [x for x in c.variables if x.name == "my_var_A"][0]

        entry_phi = [
            x
            for x in f.entry_point.irs_ssa
            if isinstance(x, Phi) and x.lvalue.non_ssa_version == var_a
        ][0]
        # The four potential sources are:
        # 1. initial value
        # 2. my_var_A = i;
        # 3. my_var_A = 3;
        # 4. phi-value after call to b.do_stuff(), which could be reentrant.
        assert len(entry_phi.rvalues) == 4

        # Locate the first high-level call (should be b.do_stuff())
        call_node = [x for y in f.nodes for x in y.irs_ssa if isinstance(x, HighLevelCall)][0]
        n = call_node.node
        # Get phi-node after call
        after_call_phi = n.irs_ssa[n.irs_ssa.index(call_node) + 1]
        # The two sources for this phi node is
        # 1. my_var_A = i;
        # 2. my_var_A = 3;
        assert isinstance(after_call_phi, Phi)
        assert len(after_call_phi.rvalues) == 2


@pytest.mark.xfail(strict=True, reason="Fails in current slither version. Fix in #1102.")
def test_storage_refers_to(slither_from_solidity_source):
    """Test the storage aspects of the SSA IR

    When declaring a var as being storage, start tracking what storage it refers_to.
    When a phi-node is created, ensure refers_to is propagated to the phi-node.
    Assignments also propagate refers_to.
    Whenever a ReferenceVariable is the destination of an assignment (e.g. s.v = 10)
    below, create additional versions of the variables it refers to record that a
    write was made. In the current implementation, this is referenced by phis.
    """
    source = """
   contract A{

    struct St{
        int v;
    }

    St state0;
    St state1;

    function f() public{
        St storage s = state0;
        if(true){
            s = state1;
        }
        s.v = 10;
    }
}
    """
    with slither_from_solidity_source(source) as slither:
        c = slither.contracts[0]
        f = c.functions[0]

        phinodes = get_ssa_of_type(f, Phi)
        # Expect 2 in entrypoint (state0/state1 initial values), 1 at 'ENDIF' and two related to the
        # ReferenceVariable write s.v = 10.
        assert len(phinodes) == 5

        # Assign s to state0, s to state1, s.v to 10
        assigns = get_ssa_of_type(f, Assignment)
        assert len(assigns) == 3

        # The IR variables have is_storage
        assert all(x.lvalue.is_storage for x in assigns if isinstance(x, LocalIRVariable))

        # s.v ReferenceVariable points to one of the phi vars...
        ref0 = [x.lvalue for x in assigns if isinstance(x.lvalue, ReferenceVariable)][0]
        sphis = [x for x in phinodes if x.lvalue == ref0.points_to]
        assert len(sphis) == 1
        sphi = sphis[0]

        # ...and that phi refers to the two entry phi-values
        entryphi = [x for x in phinodes if x.lvalue in sphi.lvalue.refers_to]
        assert len(entryphi) == 2

        # The remaining two phis are the ones recording that write through ReferenceVariable occurred
        for ephi in entryphi:
            phinodes.remove(ephi)
        phinodes.remove(sphi)
        assert len(phinodes) == 2

        # And they are recorded in one of the entry phis
        assert phinodes[0].lvalue in entryphi[0].rvalues or entryphi[1].rvalues
        assert phinodes[1].lvalue in entryphi[0].rvalues or entryphi[1].rvalues


@pytest.mark.skipif(
    not valid_version("0.4.0"), reason="Solidity version 0.4.0 not available on this platform"
)
def test_initial_version_exists_for_locals(slither_from_solidity_source):
    """
    In solidity you can write statements such as
    uint a = a + 1, this test ensures that can be handled for local variables.
    """
    src = """
    contract C {
        function func() internal {
            uint a = a + 1;
        }
    }
    """
    with slither_from_solidity_source(src, "0.4.0") as slither:
        verify_properties_hold(slither)
        c = slither.contracts[0]
        f = c.functions[0]

        addition = get_ssa_of_type(f, Binary)[0]
        assert addition.type == BinaryType.ADDITION
        assert isinstance(addition.variable_right, Constant)
        a_0 = addition.variable_left
        assert a_0.index == 0
        assert a_0.name == "a"

        assignment = get_ssa_of_type(f, Assignment)[0]
        a_1 = assignment.lvalue
        assert a_1.index == 1
        assert a_1.name == "a"
        assert assignment.rvalue == addition.lvalue

        assert a_0.non_ssa_version == a_1.non_ssa_version


@pytest.mark.xfail(strict=True, reason="Fails in current slither version. Fix in #1102.")
@pytest.mark.skipif(
    not valid_version("0.4.0"), reason="Solidity version 0.4.0 not available on this platform"
)
def test_initial_version_exists_for_state_variables(slither_from_solidity_source):
    """
    In solidity you can write statements such as
    uint a = a + 1, this test ensures that can be handled for state variables.
    """
    src = """
    contract C {
        uint a = a + 1;
    }
    """
    with slither_from_solidity_source(src, "0.4.0") as slither:
        verify_properties_hold(slither)
        c = slither.contracts[0]
        f = c.functions[0]  # There will be one artificial ctor function for the state vars

        addition = get_ssa_of_type(f, Binary)[0]
        assert addition.type == BinaryType.ADDITION
        assert isinstance(addition.variable_right, Constant)
        a_0 = addition.variable_left
        assert isinstance(a_0, StateIRVariable)
        assert a_0.name == "a"

        assignment = get_ssa_of_type(f, Assignment)[0]
        a_1 = assignment.lvalue
        assert isinstance(a_1, StateIRVariable)
        assert a_1.index == a_0.index + 1
        assert a_1.name == "a"
        assert assignment.rvalue == addition.lvalue

        assert a_0.non_ssa_version == a_1.non_ssa_version
        assert isinstance(a_0.non_ssa_version, StateVariable)

        # No conditional/other function interaction so no phis
        assert len(get_ssa_of_type(f, Phi)) == 0


@pytest.mark.xfail(strict=True, reason="Fails in current slither version. Fix in #1102.")
def test_initial_version_exists_for_state_variables_function_assign(slither_from_solidity_source):
    """
    In solidity you can write statements such as
    uint a = a + 1, this test ensures that can be handled for local variables.
    """
    # TODO (hbrodin): Could be a detector that a is not used in f
    src = """
    contract C {
        uint a = f();

        function f() internal returns(uint) {
            return a;
        }
    }
    """
    with slither_from_solidity_source(src) as slither:
        verify_properties_hold(slither)
        c = slither.contracts[0]
        f, ctor = c.functions
        if f.is_constructor_variables:
            f, ctor = ctor, f

        # ctor should have a single call to f that assigns to a
        # temporary variable, that is then assigned to a

        call = get_ssa_of_type(ctor, InternalCall)[0]
        assert call.node.function == f
        assign = get_ssa_of_type(ctor, Assignment)[0]
        assert assign.rvalue == call.lvalue
        assert isinstance(assign.lvalue, StateIRVariable)
        assert assign.lvalue.name == "a"

        # f should have a phi node on entry of a0, a1 and should return
        # a2
        phi = get_ssa_of_type(f, Phi)[0]
        assert len(phi.rvalues) == 2
        assert assign.lvalue in phi.rvalues


@pytest.mark.skipif(
    not valid_version("0.4.0"), reason="Solidity version 0.4.0 not available on this platform"
)
def test_return_local_before_assign(slither_from_solidity_source):
    src = """
    // this require solidity < 0.5
    // a variable can be returned before declared. Ensure it can be
    // handled by Slither.
    contract A {
    function local(bool my_bool) internal returns(uint){
        if(my_bool){
            return a_local;
        }

        uint a_local = 10;
    }
    }
    """
    with slither_from_solidity_source(src, "0.4.0") as slither:
        f = slither.contracts[0].functions[0]

        ret = get_ssa_of_type(f, Return)[0]
        assert len(ret.values) == 1
        assert ret.values[0].index == 0

        assign = get_ssa_of_type(f, Assignment)[0]
        assert assign.lvalue.index == 1
        assert assign.lvalue.non_ssa_version == ret.values[0].non_ssa_version


@pytest.mark.skipif(
    not valid_version("0.5.0"), reason="Solidity version 0.5.0 not available on this platform"
)
def test_shadow_local(slither_from_solidity_source):
    src = """
    contract A {
     // this require solidity 0.5
    function shadowing_local() internal{
        uint local = 0;
        {
            uint local = 1;
            {
                uint local = 2;
            }
        }
    }
    }
    """
    with slither_from_solidity_source(src, "0.5.0") as slither:
        _dump_functions(slither.contracts[0])
        f = slither.contracts[0].functions[0]

        # Ensure all assignments are to a variable of index 1
        # not using the same IR var.
        assert all(map(lambda x: x.lvalue.index == 1, get_ssa_of_type(f, Assignment)))


@pytest.mark.xfail(strict=True, reason="Fails in current slither version. Fix in #1102.")
def test_multiple_named_args_returns(slither_from_solidity_source):
    """Verifies that named arguments and return values have correct versions

    Each arg/ret have an initial version, version 0, and is written once and should
    then have version 1.
    """
    src = """
    contract A {
        function multi(uint arg1, uint arg2) internal returns (uint ret1, uint ret2) {
            arg1 = arg1 + 1;
            arg2 = arg2 + 2;
            ret1 = arg1 + 3;
            ret2 = arg2 + 4;
        }
    }"""
    with slither_from_solidity_source(src) as slither:
        verify_properties_hold(slither)
        f = slither.contracts[0].functions[0]

        # Ensure all LocalIRVariables (not TemporaryVariables) have index 1
        assert all(
            map(
                lambda x: x.lvalue.index == 1 or not isinstance(x.lvalue, LocalIRVariable),
                get_ssa_of_type(f, OperationWithLValue),
            )
        )


@pytest.mark.xfail(reason="Tests for wanted state of SSA IR, not current.", strict=True)
def test_memory_array(slither_from_solidity_source):
    src = """
    contract MemArray {
        struct A {
            uint val1;
            uint val2;
        }

        function test_array() internal {
            A[] memory a= new A[](4);
            // Create REF_0 -> a_1[2]
            accept_array_entry(a[2]);

            // Create REF_1 -> a_1[3]
            accept_array_entry(a[3]);

            A memory alocal;
            accept_array_entry(alocal);

        }

        // val_1 = ϕ(val_0, REF_0, REF_1, alocal_1)
        // val_0 is an unknown external value
        function accept_array_entry(A memory val) public returns (uint) {
            uint zero = 0;
            b(zero);
            // Create REF_2 -> val_1.val1
            return b(val.val1);
        }

        function b(uint arg) public returns (uint){
            // arg_1 = ϕ(arg_0, zero_1, REF_2)
            return arg + 1;
        }
    }"""
    with slither_from_solidity_source(src) as slither:
        c = slither.contracts[0]

        ftest_array, faccept, fb = c.functions

        # Locate REF_0/REF_1/alocal (they are all args to the call)
        accept_args = [x.arguments[0] for x in get_ssa_of_type(ftest_array, InternalCall)]

        # Check entrypoint of accept_array_entry, it should contain a phi-node
        # of expected rvalues
        [phi_entry_accept] = get_ssa_of_type(faccept.entry_point, Phi)
        for arg in accept_args:
            assert arg in phi_entry_accept.rvalues
        # NOTE(hbrodin): There should be an additional val_0 in the phi-node.
        # That additional val_0 indicates an external caller of this function.
        assert len(phi_entry_accept.rvalues) == len(accept_args) + 1

        # Args used to invoke b
        b_args = [x.arguments[0] for x in get_ssa_of_type(faccept, InternalCall)]

        # Check entrypoint of B, it should contain a phi-node of expected
        # rvalues
        [phi_entry_b] = get_ssa_of_type(fb.entry_point, Phi)
        for arg in b_args:
            assert arg in phi_entry_b.rvalues

        # NOTE(hbrodin): There should be an additional arg_0 (see comment about phi_entry_accept).
        assert len(phi_entry_b.rvalues) == len(b_args) + 1


@pytest.mark.xfail(reason="Tests for wanted state of SSA IR, not current.", strict=True)
def test_storage_array(slither_from_solidity_source):
    src = """
    contract StorageArray {
        struct A {
            uint val1;
            uint val2;
        }

        // NOTE(hbrodin): a is never written, should only become a_0. Same for astorage (astorage_0). Phi-nodes at entry
        // should only add new versions of a state variable if it is actually written.
        A[] a;
        A astorage;

        function test_array() internal {
            accept_array_entry(a[2]);
            accept_array_entry(a[3]);
            accept_array_entry(astorage);
        }

        function accept_array_entry(A storage val) internal returns (uint) {
            // val is either a[2], a[3] or astorage_0. Ideally this could be identified.
            uint five = 5;

            // NOTE(hbrodin): If the following line is enabled, there would ideally be a phi-node representing writes
            // to either a or astorage.
            //val.val2 = 4;
            b(five);
            return b(val.val1);
        }

        function b(uint value) public returns (uint){
            // Expect a phi-node at the entrypoint
            // value_1 = ϕ(value_0, five_0, REF_x), where REF_x is the reference to val.val1 in accept_array_entry.
            return value + 1;
        }
    }"""
    with slither_from_solidity_source(src) as slither:
        c = slither.contracts[0]
        _dump_functions(c)
        ftest, faccept, fb = c.functions

        # None of a/astorage is written so expect that there are no phi-nodes at entrypoint.
        assert len(get_ssa_of_type(ftest.entry_point, Phi)) == 0

        # Expect all references to start from index 0 (no writes)
        assert all(x.variable_left.index == 0 for x in get_ssa_of_type(ftest, Index))

        [phi_entry_accept] = get_ssa_of_type(faccept.entry_point, Phi)
        assert len(phi_entry_accept.rvalues) == 3  # See comment in b above

        [phi_entry_b] = get_ssa_of_type(fb.entry_point, Phi)
        assert len(phi_entry_b.rvalues) == 3  # See comment in b above


@pytest.mark.xfail(strict=True, reason="Fails in current slither version. Fix in #1102.")
def test_issue_468(slither_from_solidity_source):
    """
    Ensure issue 468 is corrected as per
    https://github.com/crytic/slither/issues/468#issuecomment-620974151
    The one difference is that we allow the phi-function at entry of f to
    hold exit state which contains init state and state from branch, which
    is a bit redundant. This could be further simplified.
    """
    source = """
    contract State {
    int state = 0;
    function f(int a) public returns (int) {
        // phi-node here for state
        if (a < 1) {
            state += 1;
        }
        // phi-node here for state
        return state;
    }
    }
    """
    with slither_from_solidity_source(source) as slither:
        c = slither.get_contract_from_name("State")[0]
        f = [x for x in c.functions if x.name == "f"][0]

        # Check that there is an entry point phi values for each later value
        # plus one additional which is the initial value
        entry_ssa = f.entry_point.irs_ssa
        assert len(entry_ssa) == 1
        phi_entry = entry_ssa[0]
        assert isinstance(phi_entry, Phi)

        # Find the second phi function
        endif_node = [x for x in f.nodes if x.type == NodeType.ENDIF][0]
        assert len(endif_node.irs_ssa) == 1
        phi_endif = endif_node.irs_ssa[0]
        assert isinstance(phi_endif, Phi)

        # Ensure second phi-function contains init-phi and one additional
        assert len(phi_endif.rvalues) == 2
        assert phi_entry.lvalue in phi_endif.rvalues

        # Find return-statement and ensure it returns the phi_endif
        return_node = [x for x in f.nodes if x.type == NodeType.RETURN][0]
        assert len(return_node.irs_ssa) == 1
        ret = return_node.irs_ssa[0]
        assert len(ret.values) == 1
        assert phi_endif.lvalue in ret.values

        # Ensure that the phi_endif (which is the end-state for function as well) is in the entry_phi
        assert phi_endif.lvalue in phi_entry.rvalues


@pytest.mark.xfail(strict=True, reason="Fails in current slither version. Fix in #1102.")
def test_issue_434(slither_from_solidity_source):
    source = """
     contract Contract {
        int public a;
        function f() public {
            g();
            a += 1;
        }

        function e() public {
            a -= 1;
        }

        function g() public {
            e();
        }
    }
    """
    with slither_from_solidity_source(source) as slither:
        c = slither.get_contract_from_name("Contract")[0]

        e = [x for x in c.functions if x.name == "e"][0]
        f = [x for x in c.functions if x.name == "f"][0]
        g = [x for x in c.functions if x.name == "g"][0]

        # Ensure there is a phi-node at the beginning of f and e
        phi_entry_e = get_ssa_of_type(e.entry_point, Phi)[0]
        phi_entry_f = get_ssa_of_type(f.entry_point, Phi)[0]
        # But not at g
        assert len(get_ssa_of_type(g, Phi)) == 0

        # Ensure that the final states of f and e are in the entry-states
        add_f = get_filtered_ssa(
            f, lambda x: isinstance(x, Binary) and x.type == BinaryType.ADDITION
        )[0]
        sub_e = get_filtered_ssa(
            e, lambda x: isinstance(x, Binary) and x.type == BinaryType.SUBTRACTION
        )[0]
        assert add_f.lvalue in phi_entry_f.rvalues
        assert add_f.lvalue in phi_entry_e.rvalues
        assert sub_e.lvalue in phi_entry_f.rvalues
        assert sub_e.lvalue in phi_entry_e.rvalues

        # Ensure there is a phi-node after call to g
        call = get_ssa_of_type(f, InternalCall)[0]
        idx = call.node.irs_ssa.index(call)
        aftercall_phi = call.node.irs_ssa[idx + 1]
        assert isinstance(aftercall_phi, Phi)

        # Ensure that phi node ^ is used in the addition afterwards
        assert aftercall_phi.lvalue in (add_f.variable_left, add_f.variable_right)


@pytest.mark.xfail(strict=True, reason="Fails in current slither version. Fix in #1102.")
def test_issue_473(slither_from_solidity_source):
    source = """
    contract Contract {
    function f() public returns (int) {
        int a = 1;
        if (a > 0) {
            a = 2;
        }
        if (a == 3) {
            a = 6;
        }
        return a;
    }
    }
    """
    with slither_from_solidity_source(source) as slither:
        c = slither.get_contract_from_name("Contract")[0]
        f = c.functions[0]

        phis = get_ssa_of_type(f, Phi)
        return_value = get_ssa_of_type(f, Return)[0]

        # There shall be two phi functions
        assert len(phis) == 2
        first_phi = phis[0]
        second_phi = phis[1]

        # The second phi is the one being returned, if it's the first swap them (iteration order)
        if first_phi.lvalue in return_value.values:
            first_phi, second_phi = second_phi, first_phi

        # First phi is for [a=1 or a=2]
        assert len(first_phi.rvalues) == 2

        # second is for [a=6 or first phi]
        assert first_phi.lvalue in second_phi.rvalues
        assert len(second_phi.rvalues) == 2

        # return is for second phi
        assert len(return_value.values) == 1
        assert second_phi.lvalue in return_value.values


def test_issue_1748(slither_from_solidity_source):
    source = """
    contract Contract {
        uint[] arr;
        function foo(uint i) public {
            arr = [1];
        }
    }
    """
    with slither_from_solidity_source(source) as slither:
        c = slither.get_contract_from_name("Contract")[0]
        f = c.functions[0]
        operations = f.slithir_operations
        assign_op = operations[0]
        assert isinstance(assign_op, InitArray)


def test_issue_1776(slither_from_solidity_source):
    source = """
    contract Contract {
        function foo() public returns (uint) {
            uint[5][10][] memory arr = new uint[5][10][](2);
            return 0;
        }
    }
    """
    with slither_from_solidity_source(source) as slither:
        c = slither.get_contract_from_name("Contract")[0]
        f = c.functions[0]
        operations = f.slithir_operations
        new_op = operations[0]
        lvalue = new_op.lvalue
        lvalue_type = lvalue.type
        assert isinstance(lvalue_type, ArrayType)
        assert lvalue_type.is_dynamic
        lvalue_type1 = lvalue_type.type
        assert isinstance(lvalue_type1, ArrayType)
        assert not lvalue_type1.is_dynamic
        assert lvalue_type1.length_value.value == "10"
        lvalue_type2 = lvalue_type1.type
        assert isinstance(lvalue_type2, ArrayType)
        assert not lvalue_type2.is_dynamic
        assert lvalue_type2.length_value.value == "5"


def test_issue_1846_ternary_in_if(slither_from_solidity_source):
    source = """
    contract Contract {
        function foo(uint x) public returns (uint y) {
            if (x > 0) {
                y = x > 1 ? 2 : 3;
            } else {
                y = 4;
            }
        }
    }
    """
    with slither_from_solidity_source(source) as slither:
        c = slither.get_contract_from_name("Contract")[0]
        f = c.functions[0]
        node = f.nodes[1]
        assert node.type == NodeType.IF
        assert node.son_true.type == NodeType.IF
        assert node.son_false.type == NodeType.EXPRESSION


def test_issue_1846_ternary_in_ternary(slither_from_solidity_source):
    source = """
        contract Contract {
            function foo(uint x) public returns (uint y) {
                y = x > 0 ? x > 1 ? 2 : 3 : 4;
            }
        }
        """
    with slither_from_solidity_source(source) as slither:
        c = slither.get_contract_from_name("Contract")[0]
        f = c.functions[0]
        node = f.nodes[1]
        assert node.type == NodeType.IF
        assert node.son_true.type == NodeType.IF
        assert node.son_false.type == NodeType.EXPRESSION


def test_issue_2016(slither_from_solidity_source):
    source = """
    contract Contract {
        function test() external {
            int[] memory a = new int[](5);
        }
    }
    """
    with slither_from_solidity_source(source) as slither:
        c = slither.get_contract_from_name("Contract")[0]
        f = c.functions[0]
        operations = f.slithir_operations
        new_op = operations[0]
        assert isinstance(new_op, NewArray)
        lvalue = new_op.lvalue
        lvalue_type = lvalue.type
        assert isinstance(lvalue_type, ArrayType)
        assert lvalue_type.type == ElementaryType("int256")
        assert lvalue_type.is_dynamic


def test_issue_2210(slither_from_solidity_source):
    source = """
    contract C {
    function f (int x) public returns(int) {
        int h = 1;
        int k = 5;
        int[5] memory arr = [x, C.x, C.y, h - k, h + k];
    }
    int x= 4;
    int y = 5;
    }
    """
    with slither_from_solidity_source(source) as slither:
        c = slither.get_contract_from_name("C")[0]
        f = c.functions[0]
        operations = f.slithir_operations
        new_op = operations[6]
        assert isinstance(new_op, InitArray)
        lvalue = new_op.lvalue
        lvalue_type = lvalue.type
        assert isinstance(lvalue_type, ArrayType)
        assert lvalue_type.type == ElementaryType("int256")
        assert not lvalue_type.is_dynamic

    source2 = """
    contract X {
        function _toInts(uint256[] memory a) private pure returns (int256[] memory casted) {
            /// @solidity memory-safe-assembly
            assembly {
                casted := a
            }
        }
    }
    """
    with slither_from_solidity_source(source2) as slither:
        x = slither.get_contract_from_name("X")[0]
        f2 = x.functions[0]
        operations = f2.slithir_operations
        new_op2 = operations[0]
        assert isinstance(new_op2, Assignment)

        lvalue = new_op2.lvalue
        lvalue_type = lvalue.type
        assert isinstance(lvalue_type, ArrayType)
        assert lvalue_type.type == ElementaryType("int256")
        assert lvalue_type.is_dynamic

        rvalue_type = new_op2.rvalue.type
        assert isinstance(rvalue_type, ArrayType)
        assert rvalue_type.type == ElementaryType("uint256")
        assert rvalue_type.is_dynamic
